<?xml version='1.0' encoding='utf-8'?>
<?xml-stylesheet type="text/xsl" href="/sheet.xsl"?><rss version="2.0"><channel><item><title>피쳐 토글 - 빠르고 안정적인 릴리즈를 향한 도약</title><link>https://tech.mfort.co.kr/blog/2022-11-24-feature-toggle/</link><ns0:encoded xmlns:ns0="http://purl.org/rss/1.0/modules/content/">&lt;div class="prose" morss_own_score="2.7970149253731345" morss_score="138.78511016346837"&gt;
&lt;p&gt;안녕하세요, 맘편한세상의 백엔드 엔지니어 이재찬입니다.&lt;/p&gt;
&lt;p&gt;맘편한세상의 백엔드 팀은 빠르고 안정적인 릴리즈를 가능케 하는 도구로 피쳐 토글(feature toggle)을 사용하고 있습니다. 이 글에서는 피쳐 토글이 어떤 개념인지, 어떤 상황에서 사용하면 좋은지, 그리고 맘편한세상에서는 실제로 어떻게 사용하고 있는지를 공유해보고자 합니다.&lt;/p&gt;
&lt;h2&gt;피쳐 토글의 정의&lt;/h2&gt;
&lt;p&gt;피쳐 토글(또는 피쳐 플래그)을 한 마디로 축약하자면, 특정 코드의 실행 여부를 코드 외부에서 제어할 수 있게 하는 시스템입니다. 코드로 표현한다면, 아래와 같은 예시를 생각해볼 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;// isEnabled 함수는 서버 외부 설정에 따라, true / false중 하나를 반환&lt;/span&gt;
&lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;isEnabled&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;span&gt;awesome-feature&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;span&gt;))&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;provideAwesomeFeature&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt; &lt;span&gt;else&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;provideLegacyFeature&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;isEnabled&lt;/code&gt; 함수가 어떤 값을 반환할지는 서버 외부 설정으로 변경할 수 있습니다. 예를 들어서, 개발자가 어드민 페이지에서 &lt;code&gt;awesome-feature&lt;/code&gt;를 활성화하면, 서버에서는 &lt;code&gt;provideAwesomeFeature&lt;/code&gt;가 실행되고, 비활성화하면 &lt;code&gt;provideLegacyFeature&lt;/code&gt; 가 실행되는 방식입니다. 서버 배포 없이도, 둘 중에 어떤 함수가 실행될지 결정할 수 있는 것이죠.&lt;/p&gt;
&lt;p&gt;여기서 조금 더 나아가서는, &lt;code&gt;isEnabled&lt;/code&gt; 함수가 호출 환경에 관련된 값을 받아서, 피쳐 토글의 반환값이 호출 환경마다 (예를 들어서, 기능을 요청하는 유저마다) 다른 값을 반환하게 할 수도 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;isEnabled&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;span&gt;awesome-feature&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;context&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;userId&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;id&lt;/span&gt; &lt;span&gt;}&lt;/span&gt; &lt;span&gt;}))&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;provideAwesomeFeature&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt; &lt;span&gt;else&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;provideLegacyFeature&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;피쳐 토글의 정의 자체는 이렇게 간단하게 설명할 수 있습니다. 하지만 피쳐 토글이라는 개념이 왜 필요한지, 언제 사용하면 좋을지는 조금 더 흥미로운 문제입니다.&lt;/p&gt;
&lt;h2&gt;배포와 릴리즈의 분리&lt;/h2&gt;
&lt;p&gt;피쳐 토글이 제공하는 기능의 본질은 “배포와 릴리즈를 분리할 수 있는 능력”입니다. “배포” 와 “릴리즈” 이 두 단어는 종종 혼용되지만, 여기서는 이 두 개념의 좀 더 정확한 정의와 특성을 한번 짚고 넘어가도록 하겠습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;배포(deployment): 코드가 서버에 배치되는 것 / 인프라 관점에서의 용어&lt;/li&gt;
&lt;li&gt;릴리즈(release): 기능이 사용자에게 제공되는 것 / 비즈니스 관점에서의 용어&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;일반적으로는, 코드는 배포되는 즉시 해당하는 기능들이 모든 사용자에게 제공됩니다. 즉, 배포와 릴리즈가 동일한 시점에 발생하는 것이죠. 피쳐 토글을 사용한다면, 코드를 배포했을 때 그 코드에 해당하는 기능이 즉시 릴리즈가 될 필요가 없습니다. 코드가 배포된 이후, 개발자가 기능이 릴리즈되기를 원할 때 피쳐 토글을 활성화하면, 그 때부터 기능이 사용자에게 릴리즈됩니다. 즉, 배포와 릴리즈의 분리가 달성되는 것입니다.&lt;/p&gt;
&lt;p&gt;피쳐 토글을 사용해서 배포와 릴리즈의 결합을 끊어내는 것은, 크게 두 가지 상황에서 이점을 가져다줄 수 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;새로운 기능을 릴리즈할 때&lt;/li&gt;
&lt;li&gt;작업중인 코드를 통합할 때&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 두 가지 상황에 대해서 조금 더 자세히 살펴봅시다.&lt;/p&gt;
&lt;h2&gt;새로운 기능을 피쳐 토글로 릴리즈&lt;/h2&gt;
&lt;p&gt;피쳐 토글을 올바르게 사용한다면, 새로운 기능이 릴리즈될 때의 안정성을 크게 개선할 수 있습니다. 새롭게 작업한 기능이 운영환경에 안전하게 릴리즈될 수 있게 하기 위해서, 통상적으로는 아래의 두 가지 방식을 통해서 새로운 기능의 안정성을 검증합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;운영 환경과 최대한 비슷한 스테이지 환경에 배포해, 개발자와 QA 등 내부 사용자가 테스트 한 후, 이상이 없으면 운영 배포&lt;/li&gt;
&lt;li&gt;카나리 배포 전략을 통해서, 일시적으로 새로운 기능을 일부 유저에게만 배포한 후, 이상이 없다면 그 후 전체 배포&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 두가지 방식은 분명 널리 쓰이는 방식이지만, 기능의 안정성을 검증하기에는 약간의 아쉬운 구석들이 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;스테이지 환경에 배포해서 테스트하는 것은 운영과 스테이지 환경의 차이를 감수해야 하고, 또한 내부 사용자의 테스트를 통해서만 피드백을 얻을 수 있습니다.&lt;/li&gt;
&lt;li&gt;카나리 배포는 운영 환경에서 실제 사용자 대상으로 피드백을 받을 수 있지만, 인프라 레벨에서 기존 서버와 신기능 서버를 나누기 때문에 세밀하게 대상 유저를 조정하기 어렵고, 여러 기능에 대해 동시에 검증을 진행하기도 어렵습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;만약 배포와 릴리즈를 분리해서 새로운 기능의 릴리즈 여부를 피쳐 토글로 제어한다면, 기존 방식과 대비해서 기능 검증과 릴리즈가 더욱 유연하고 안전해집니다. 먼저 운영환경에서 일부 유저 대상으로만 기능을 릴리즈해서 피드백을 받을 수도 있고, 릴리즈 대상을 내부 유저 / 일반 유저들 중에서 임의로 결정할 수도 있습니다. 여러 기능을 동시에 검증하는 것도 가능합니다. 뿐만 아니라, 새로운 기능의 문제가 발견되었을 때, 별도의 코드 수정이나 배포 없이 빠르게 릴리즈 취소가 가능하기 때문에, 릴리즈로 인해 발생하는 문제의 영향도를 크게 줄일 수도 있습니다.&lt;/p&gt;
&lt;h2&gt;작업 중인 코드를 피쳐 토글로 통합&lt;/h2&gt;
&lt;p&gt;피쳐 토글을 사용한다면, 작업 중인 기능의 코드를 운영 환경에 배포해두고, 외부 유저 대상으로는 새로운 코드가 실행되지 않도록 제어할 수 있습니다. 이렇게 하면 기능 작업 기간이 길더라도, 개발 도중에 언제든지 메인 브랜치에 통합해두거나, 다른 운영 기능들과 함께 운영 서버에 배포하는 것이 자유롭습니다. 기능 준비가 완료되면, 그 때 피쳐 토글을 통해 기능을 릴리즈하면 됩니다.&lt;/p&gt;
&lt;p&gt;작업 중인 기능에 대한 배포를 미리 잘게 쪼개서 수행하는 것은 여러 가지 이점을 가져다줍니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;현재 작업중인 코드를 팀원이 활용하기 편리해집니다. 작업중인 기능에서 기존 공용 코드를 수정하거나, 새로운 공용 코드를 추가할 수도 있는데, 기능 전체를 미리 배포해둘 수 있으면 컨플릭트를 최소화하면서 공용 코드를 팀원과 쉽게 공유할 수 있습니다.&lt;/li&gt;
&lt;li&gt;작업 중에 내부 사용자 대상으로 테스트를 진행하기 더 용이합니다. 큰 작업의 일부분이 완료되었을 때, 완료된 부분에 대해서만 개발자나 QA 등 내부 사용자 대상으로 기능을 오픈해 피드백을 얻을 수 있습니다.&lt;/li&gt;
&lt;li&gt;트렁크 기반 개발 방식으로 개발하도록 돕습니다. 트렁크 기반 개발을 한 마디로 설명하자면, 개발자들이 단일 메인 브랜치에 자신의 코드를 자주 통합하는 것을 지향하는 브랜칭 전략입니다. 피쳐 토글의 도움을 받아 트렁크 기반 방식으로 개발한다면, 머지 컨플릭트 감소, 빠른 릴리즈 주기 등 여러 이점을 얻을 수 있습니다. &lt;em&gt;(트렁크 기반 개발에 대한 더 자세한 정보는, 제가 작성한 다른 글인 &lt;a href="https://tech.mfort.co.kr/blog/2022-08-05-trunk-based-development/"&gt;Git Flow에서 트렁크 기반 개발으로 나아가기&lt;/a&gt; 에서 확인할 수 있습니다! )&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;맘편한세상에서 사용하는 피쳐 토글 서비스&lt;/h2&gt;
&lt;p&gt;맘편한세상 백엔드 팀에서는 현재 &lt;a href="https://www.getunleash.io/"&gt;Unleash&lt;/a&gt;라는 피쳐 토글 서비스를 사용하고 있습니다. 이 서비스는 작년 10월에 맘편한세상 백엔드 팀에서 피쳐 토글을 도입할 때부터 사용해왔고, 현재까지도 매우 만족하면서 사용하고 있습니다. Unleash 서비스에서 특히 만족하는 부분들은 아래와 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;낮은 호출 오버헤드: Unleash에서 제공하는 서버 SDK를 사용한다면, SDK가 피쳐 토글 설정을 메모리에 미리 저장하고 있다가, &lt;code&gt;isEnabled&lt;/code&gt;함수가 호출될 때 저장된 피쳐 토글 설정을 통해 활성화 / 비활성화 여부를 서버 내에서 계산합니다. 즉, &lt;code&gt;isEnabled&lt;/code&gt;함수 호출마다 네트워크 요청이 발생하지 않아, 호출 오버헤드가 매우 낮습니다.&lt;/li&gt;
&lt;li&gt;빠른 설정 전파 속도: Unleash SDK는 Unleash 서버를 주기적으로 폴링해서(기본값은 15초마다) 설정을 가져와 메모리에 저장하기 때문에, 피쳐 토글 활성화 여부를 변경하고 폴링 주기 만큼만 기다리면, 변경될 피쳐 토글 설정이 서버에 반영되는 것을 확인할 수 있었습니다. 폴링 주기 또한 커스텀한 설정이 가능합니다.&lt;/li&gt;
&lt;li&gt;합리적인 비용: 서비스 코드가 오픈소스로 공개되어 있고, 서비스를 자체 인프라에 배포하는 것도 매우 편리해서, 인프라 비용만 감당하고 저희 인프라에 배포해서 사용하고 있습니다.&lt;/li&gt;
&lt;li&gt;어드민 패널: 직관적이고 편리한 어드민 패널을 제공해서, 쉽게 피쳐 토글 설정을 변경할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src="https://tech.mfort.co.kr/assets/images/20221124/unleash-list.png"&gt;
&lt;em&gt;Unleash의 피쳐 토글 리스트 페이지. 각 피쳐 토글이 가장 최근에 언제 호출되었는지도 확인할 수 있습니다.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tech.mfort.co.kr/assets/images/20221124/unleash-show.png"&gt;
&lt;em&gt;Unleash의 개별 피쳐 토글 설정 페이지. 각 환경에서 토글이 활성화되었는지, 얼마나 자주 토글이 요청되는지 확인 가능합니다.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;피쳐 토글 서비스 도입을 검토하거나, 자체 구현을 고민하고 있다면, 위와 같은 요소들을 충족할 수 있을지 생각하면서 결정하면 좋을 것입니다.&lt;/p&gt;
&lt;h2&gt;실제 사용 사례&lt;/h2&gt;
&lt;p&gt;아래에서는 맘편한세상 백엔드 팀에서 피쳐 토글을 사용하고 있는 다양한 예시를 보여드리고자 합니다.&lt;/p&gt;
&lt;h3&gt;배포에 이상이 있을 때, 피쳐 토글을 통해서 빠르게 릴리즈 취소&lt;/h3&gt;
&lt;p&gt;새롭게 작업한 기능으로 인해 예상하지 못한 에러가 발생해 Sentry 알림이 온 상황입니다. 기능 릴리즈를 피쳐 토글을 통해 진행했었기 때문에, 다시 피쳐 토글을 이용해 빠르게 기능을 비활성화하고, 문제를 그 후에 디버깅할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tech.mfort.co.kr/assets/images/20221124/toggle-example-error.png"&gt;&lt;/p&gt;
&lt;h3&gt;배포를 미리 해놓고, 릴리즈는 피쳐 토글 활성화로 간편하게&lt;/h3&gt;
&lt;p&gt;특정 기능의 릴리즈를 위해서 백엔드와 프론트 릴리즈가 모두 필요한 상황입니다. 이 때 서버와 프론트에서 모두 미리 배포를 해두고, 정해진 릴리즈 시간에 서버와 프론트 모두 피쳐 토글을 활성화해서, 원하는 시간에 쉽게 릴리즈를 하고, QA 검증 또한 바로 순조롭게 진행할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tech.mfort.co.kr/assets/images/20221124/toggle-example-payment.png"&gt;&lt;/p&gt;
&lt;h3&gt;작업 중인 태스크에 대해 미리 배포 후 테스트&lt;/h3&gt;
&lt;p&gt;맘시터 앱 내 채팅 관련 POC(Proof Of Concept) 기능에 관한 PR입니다. 해당 기능을 이어서 개발하기 전에 스테이지에서 테스트해보기 위해, 기능을 아래와 같이 피쳐 토글을 통해 보호된 상태로 메인 브랜치에 머지했습니다. 저희는 &lt;a href="https://tech.mfort.co.kr/blog/2022-08-05-trunk-based-development/"&gt;트렁크 기반 개발&lt;/a&gt; 방식을 사용하고 있기 때문에, 코드는 바로 운영까지 배포되었지만, 해당 기능은 스테이지에서만 피쳐 토글을 사용해 안전하게 테스트할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tech.mfort.co.kr/assets/images/20221124/toggle-example-poc.png"&gt;&lt;/p&gt;
&lt;h3&gt;기존 엔드포인트를 새로운 엔드포인트로 점진적으로 대체&lt;/h3&gt;
&lt;p&gt;현재 맘시터 백엔드 팀에서는 레거시 Node 서버와 새로운 Kotlin 서버를 동시에 운영하고 있고, 점진적으로 Kotlin 서버로 Node 서버를 대체해 나가는 것을 목표로 하고 있습니다. 현재 두 서버 앞단에는 Spring Gateway를 두고 있습니다.&lt;/p&gt;
&lt;p&gt;Gateway에서 커스텀한 Route Predicate를 만들어서, 특정 요청이 Node 서버로 갈지, Kotlin 서버로 갈지를 피쳐 토글을 통해 결정할 수 있게 작업했습니다. 이를 통해 기존 Node 엔드포인트를 대체하는 새로운 엔드포인트를 Kotlin 서버에 배포해 두고, 필요할 때만 새로운 엔드포인트가 호출되도록 해, 새로운 엔드포인트를 안정적으로 운영과 스테이지에서 테스트할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tech.mfort.co.kr/assets/images/20221124/toggle-example-gateway.png"&gt;
&lt;em&gt;Unleash를 사용하는 RoutePredicateFactory&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tech.mfort.co.kr/assets/images/20221124/toggle-example-yml.png"&gt;
&lt;em&gt;해당 predicate를 이용해서, 검색 API를 점진적으로 Kotlin서버에 있는 엔드포인트로 대체하는 중&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;일부 지역의 유저에게만 기능 릴리즈&lt;/h3&gt;
&lt;p&gt;최근에 새롭게 개편된 홈 화면을 릴리즈했었는데, 새로운 홈 화면에는 특정 지역에서만 사용할 수 있는 기능이 포함되어 있어 해당 지역의 유저만 홈 화면을 볼 수 있도록 작업이 되어야 하는 상황이었습니다. 또한, 운영팀의 상황에 따라 해당 기능을 사용할 수 있는 지역이 자주 변화하는 상황이었습니다.&lt;/p&gt;
&lt;p&gt;이 때, Unleash의 &lt;a href="https://docs.getunleash.io/reference/custom-activation-strategies"&gt;Custom Strategy&lt;/a&gt; 기능을 사용해서, Unleash에서 설정된 지역에 있는 유저에 대해서만 새로운 홈 화면을 릴리즈할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tech.mfort.co.kr/assets/images/20221124/toggle-example-location.png"&gt;
&lt;em&gt;Unleash 어드민에서, 새로운 홈 화면이 노출되는 지역 설정 가능&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tech.mfort.co.kr/assets/images/20221124/toggle-example-admin.png"&gt;
&lt;em&gt;자체 어드민과 Unleash API를 연동해서, 운영팀이 어드민에서 손쉽게 새로운 홈 화면이 노출되는 지역을 설정할 수 있음&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;이에 관한 더 자세한 내용은 저희 팀의 백엔드 개발자 현구님이 작성해 주신 블로그에서도 확인하실 수 있습니다. &lt;a href="https://hyeon9mak.github.io/unleash-custom-strategy/"&gt;Unleash custom strategy를 이용한 feature toggle 변수 필터 지정&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;마치며&lt;/h2&gt;
&lt;p&gt;위의 예시들과 같이, 저희 맘편한세상에서는 피쳐 토글 도입을 통해, 기존과 다른 방식으로 신규 기능을 더욱 빠르고 안전하게 검증하고 배포할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;만약 배포와 릴리즈가 강하게 결합된 환경으로 인해 어려움을 겪고 있다면, 피쳐 토글 도입을 강력하게 추천드리고 싶습니다. 이상으로 글을 마치겠습니다!&lt;/p&gt;
&lt;h2&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://martinfowler.com/articles/feature-toggles.html"&gt;Feature Toggles (aka Feature Flags)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://trunkbaseddevelopment.com/feature-flags/"&gt;Trunk Based Development: Feature flags&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ebook.insightbook.co.kr/book/91"&gt;클린 애자일 - 로버트 C. 마틴&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;3장. 비즈니스 실천 방법&lt;/code&gt;에서는 작은 릴리즈를 애자일 실천 방법 중 하나로 제시하고, 릴리즈와 배포 사이의 관계를 끊어야 릴리스 주기를 줄일 수 있다고 언급합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
</ns0:encoded></item><item><title>Git Flow에서 트렁크 기반 개발으로 나아가기</title><link>https://tech.mfort.co.kr/blog/2022-08-05-trunk-based-development/</link><ns0:encoded xmlns:ns0="http://purl.org/rss/1.0/modules/content/">&lt;div class="prose" morss_own_score="2.7210300429184553" morss_score="189.53615609333863"&gt;
&lt;p&gt;안녕하세요, 맘편한세상의 백엔드 엔지니어 이재찬입니다.&lt;/p&gt;
&lt;p&gt;맘편한세상의 백엔드 팀은 최근 1년 동안 Git Flow의 대안으로서 트렁크 기반 개발을 활용해 왔습니다.
이 글에서는 두 브랜치 전략의 이론적 사용법에 대해서, 그리고 실제로 사용해오면서 느낀 점들을 공유해보고자 합니다.&lt;/p&gt;
&lt;h2&gt;Git Flow&lt;/h2&gt;
&lt;p&gt;Git Flow는 Vincent Driessen이 2010년에 제시한 Git 브랜치 전략으로 널리 알려져 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://nvie.com/posts/a-successful-git-branching-model/"&gt;A successful Git branching model - Vincent Driessen&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tech.mfort.co.kr/assets/images/20220805/git-flow-original.png"&gt;
&lt;em&gt;Git Flow를 설명하는 그래프&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Git Flow 전략의 개요를 간단히 한번 짚고 넘어가 보겠습니다. Git Flow의 핵심은 &lt;code&gt;master&lt;/code&gt;와 &lt;code&gt;develop&lt;/code&gt; 두 개의 주 브랜치입니다. &lt;code&gt;master&lt;/code&gt; 브랜치에 있는 작업은 운영환경에 배포될 수 있는 것, &lt;code&gt;develop&lt;/code&gt; 브랜치에 있는 작업은 다음 릴리즈를 위해 준비된 것입니다. 이 외에도 &lt;code&gt;feature&lt;/code&gt;, &lt;code&gt;release&lt;/code&gt;, &lt;code&gt;hotfix&lt;/code&gt; 이 세 가지 보조 브랜치가 존재합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;feature&lt;/code&gt;: 새로운 기능을 &lt;code&gt;develop&lt;/code&gt; 브랜치 위에서 개발하기 위해 사용&lt;/li&gt;
&lt;li&gt;&lt;code&gt;release&lt;/code&gt;: &lt;code&gt;develop&lt;/code&gt;의 변경사항을 운영환경에 배포하기 위해 사용&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hotfix&lt;/code&gt;: 운영환경에 시급하게 수정되어야 할 이슈가 있을 때, &lt;code&gt;master&lt;/code&gt;를 빠르게 변경하기 위해 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Git Flow의 어려움&lt;/h2&gt;
&lt;p&gt;Vincent Driessen이 2010년에 Git Flow를 처음 제시했던 블로그 글을 보면, 그가 글을 작성한 지 10년 뒤인 2020년에 추가한 &lt;em&gt;Note Of Reflection&lt;/em&gt; 을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;이 부분에서는 Git Flow가 웹 앱(web apps) 개발을 염두에 두지 않고 만들어졌고, 적합하지 않다고 회고합니다. 즉 그의 말에 따르면 우리가 일반적으로 작성하는 백엔드, 프론트 애플리케이션은 Git Flow를 사용하기 적합하지 않다는 것이죠.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tech.mfort.co.kr/assets/images/20220805/git-flow-reflection.png"&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Git Flow의 어려움&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;그렇다면 어떤 이유에서 Git Flow가 웹 앱 개발에 적합하지 않다는 것일까요? 제가 생각하는 Git Flow의 어려움은 아래와 같습니다.&lt;/p&gt;
&lt;h3&gt;브랜치 관리 규약이 복잡하다&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://nvie.com/posts/a-successful-git-branching-model/"&gt;A successful Git branching model&lt;/a&gt; 에서는 &lt;code&gt;feature&lt;/code&gt;, &lt;code&gt;release&lt;/code&gt;, &lt;code&gt;hotfix&lt;/code&gt; 세 가지의 보조 브랜치가 어디서 생성될 수 있으며, 어디로 머지(Merge)되어야 하는지, 그리고 어떤 이름을 가질 수 있는지 상세하게 설명합니다. 이러한 규약을 익히는 것도 어려운 점 중 하나이지만, 규약으로 인해 브랜치 관리의 본질적인 복잡성이 증가하는 것 또한 문제가 됩니다. 이러한 복잡성을 보여주는 예시를 간단히 짚어보자면 아래와 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;release&lt;/code&gt; 와 &lt;code&gt;hotfix&lt;/code&gt; 브랜치를 머지하기 복잡합니다. 기본적으로 두 종류의 브랜치는 변경 사항이 덮어씌워지는 것을 방지하기 위해 &lt;code&gt;master&lt;/code&gt;와 &lt;code&gt;develop&lt;/code&gt;에 동시에 머지되어야 하며, 활성화된 &lt;code&gt;release&lt;/code&gt; 브랜치가 있다면 이것도 신경써야 합니다.&lt;/li&gt;
&lt;li&gt;새로운 작업을 &lt;code&gt;feature&lt;/code&gt; 브랜치와 &lt;code&gt;hotfix&lt;/code&gt; 브랜치 중 어떤 것으로 해야 할지 판단하기 어렵습니다. 일반적으로는 계획된 기능을 &lt;code&gt;feature&lt;/code&gt;, 계획되지 않은 픽스를 &lt;code&gt;hotfix&lt;/code&gt;로 하지만 이런 분류가 애매해지는 순간이 분명 존재합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hotfix&lt;/code&gt;에서 새로 발생한 변경 사항을 &lt;code&gt;feature&lt;/code&gt; 브랜치에 적용하고 싶다면, 최소 3번의 머지가 필요합니다. (&lt;code&gt;hotfix&lt;/code&gt; -&amp;gt; &lt;code&gt;master&lt;/code&gt;, &lt;code&gt;hotfix&lt;/code&gt; -&amp;gt; &lt;code&gt;develop&lt;/code&gt;, &lt;code&gt;develop&lt;/code&gt; -&amp;gt; &lt;code&gt;feature&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;브랜치가 오래 유지된다&lt;/h3&gt;
&lt;p&gt;Git Flow에서는 브랜치가 보통 오래 유지되고, 상대적으로 많은 변경사항을 한 번에 머지하는 것을 선호합니다. 이러한 선호는 여러가지 부작용을 같이 야기합니다.&lt;/p&gt;
&lt;p&gt;먼저, 서로 다른 두 브랜치에서 독립적으로 작업을 하게 되면 동시에 수정한 부분이 생길 수 있는데, 이 때 두 브랜치를 머지하기 위해서는 Git 상에서 컨플릭트를 해결해야 합니다. 두 브랜치가 각각 독립적으로 오래 작업되었을수록, 컨플릭트는 보통 더 크고 해결하기 어려워집니다. 컨플릭트 해결 중 발생하는 휴먼 에러로 버그를 만들 확률 또한, 컨플릭트의 규모가 커질수록 같이 증가합니다.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tech.mfort.co.kr/assets/images/20220805/merge-branch.png"&gt;
&lt;em&gt;오랫동안 작업한 두 브랜치를 머지하면… 💥&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;그 뿐만이 아니라, 브랜치가 오래 유지되어서 변경사항이 많을수록, 동료 개발자가 변경사항에 대해 코드리뷰를 진행하기가 더욱 어려워집니다. 동료 개발자가 코드 퀄리티나 버그에 대한 피드백을 주기 어려워지면, 전체적인 개발 문화에도 영향을 주게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tech.mfort.co.kr/assets/images/20220805/code-review.jpeg"&gt;
&lt;em&gt;코드 리뷰의 아이러니&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;마지막으로, 브랜치가 오래 유지된다는 것은 자연스레 배포의 주기가 길어진다는 것을 의미합니다. 이는 운영 배포를 통해 실제 환경에서 새롭게 만든 코드와 기능에 대한 피드백을 받는 것을 어렵게 합니다.&lt;/p&gt;
&lt;h2&gt;트렁크 기반 개발으로 해결해보자&lt;/h2&gt;
&lt;p&gt;트렁크 기반 개발은 Git Flow의 대안으로서 주로 사용되는 브랜치 전략입니다. 트렁크 기반 개발을 소개하는 &lt;a href="https://trunkbaseddevelopment.com/"&gt;trunkbaseddevelopment.com&lt;/a&gt;에서는 트렁크 기반 개발을 아래와 같이 한 문단으로 설명합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A source-control branching model, where developers collaborate on code in a single branch called ‘trunk’, resist any pressure to create other long-lived development branches by employing documented techniques. They therefore avoid merge hell, do not break the build, and live happily ever after.&lt;/p&gt;
&lt;p&gt;일종의 코드 관리 브랜칭 모델이며, 여기서는 개발자들이 ‘트렁크’라고 불리는 단일 브랜치 위에서 협력하고, 오래 유지되는 개발 브랜치를 만들게 하는 압력에 저항하기 위해 위해 설명된 테크닉을 사용합니다. 그러므로 그들은 머지 헬을 피하고, 빌드를 깨트리지 않고, 영원히 행복하게 살아갑니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;em&gt;&lt;img src="https://tech.mfort.co.kr/assets/images/20220805/tbd.png"&gt;&lt;/em&gt;
&lt;em&gt;트렁크 기반 개발&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;한 문단만으로는 설명이 쉽지 않네요. 조금 더 알아볼까요?&lt;/p&gt;
&lt;h3&gt;트렁크 기반 개발의 실천법&lt;/h3&gt;
&lt;p&gt;트렁크 기반 개발에서는 &lt;code&gt;main&lt;/code&gt;(또는 &lt;code&gt;trunk&lt;/code&gt;)이라는 주 브랜치 하나만 운영합니다. 신규 피쳐는 &lt;code&gt;main&lt;/code&gt;에 바로 커밋하거나, 며칠 내로 &lt;code&gt;main&lt;/code&gt;에 머지할 피쳐 브랜치에서 작업합니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;main&lt;/code&gt; 브랜치에 코드가 머지되었다면, 먼저 자동화된 CI 시스템이 &lt;code&gt;main&lt;/code&gt;브랜치에 대해 테스트 / 통합 과정을 통과하는지 확인합니다. 문제가 없다면, &lt;code&gt;main&lt;/code&gt; 브랜치의 코드가 그 즉시 운영 환경에 배포됩니다.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;(이는 큰 틀에서 &lt;a href="https://docs.github.com/en/get-started/quickstart/github-flow"&gt;Github Flow&lt;/a&gt;와의 실천법과도 일치하기 때문에, Github Flow 또한 트렁크 기반 개발의 일종이라고 할 수 있겠습니다.)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;이러한 간단한 방식은 Git Flow의 많은 문제들을 해결합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;브랜치 관리에 드는 리소스가 대폭 절약됩니다. 개발자가 각자 자신이 맡은 피쳐 브랜치를 &lt;code&gt;main&lt;/code&gt; 브랜치와 싱크를 맞추는 것만으로도 충분합니다.&lt;/li&gt;
&lt;li&gt;며칠 단위로 main에 머지하기 때문에, 머지 시 발생하는 변경이 작아집니다. 따라서 컨플릭트는 보통 작거나 없고, 코드 리뷰도 용이합니다.&lt;/li&gt;
&lt;li&gt;배포하기 위해서는 &lt;code&gt;main&lt;/code&gt;에 머지하는 것만으로 충분합니다. 배포 프로세스가 간단해져서, 더욱 자주 배포할 수 있게 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;트렁크 기반 개발의 어려움&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;이런 간단한 방식으로, 우리는 Git Flow의 어려움을 모두 해결하고 영원히 행복하게 살았답니다. 끝!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;…처럼 전래동화 스타일로 끝낼 수 있으면 참 좋았겠지만, 아쉽게도 제 경험상 트렁크 기반 개발 또한 모든 것을 마법처럼 해결하지는 못합니다. 일반적으로 트렁크 기반 개발의 실천법을 실행하다 보면, 아래와 같이 크게 세 가지 상황에서 어려움을 맞닥트립니다.&lt;/p&gt;
&lt;h3&gt;큰 기능 문제&lt;/h3&gt;
&lt;p&gt;만들고자 하는 기능이 몇 주 내지는 몇 달의 개발 기간이 필요한 큰 기능일 수 있습니다. 이런 상황에서는 트렁크 기반 개발의 실천법대로 며칠마다 메인 브랜치에 머지하기 어려워집니다.&lt;/p&gt;
&lt;h3&gt;의존적인 기능 문제&lt;/h3&gt;
&lt;p&gt;만들고자 하는 기능 A가 다른 기능 B의 릴리즈에 의존적이라면, B 기능이 완료되기 전까지 A 기능도 배포하기 어렵습니다.&lt;/p&gt;
&lt;h3&gt;불안정한 기능 문제&lt;/h3&gt;
&lt;p&gt;기능을 별도의 스테이지 서버에서 검증하고 배포하는 것이 아니라 바로 실서버로 배포하기 때문에, 잘못된 기능이 메인 브랜치로 머지될 위험이 증가합니다.&lt;/p&gt;
&lt;h2&gt;그럼에도 불구하고..&lt;/h2&gt;
&lt;p&gt;트렁크 기반 개발에도 분명히 어려움이 존재하지만, 위의 어려움을 해결하는 나름의 방법 또한 존재합니다. 아래에서 트렁크 기반 개발을 용이하게 하는 세 가지 방법을 설명드리겠습니다. 이 방법들은 서로 상호 보완적이며, 더 많이, 더 잘 도입할수록 트렁크 기반 개발의 어려움을 더 잘 해소할 수 있습니다.&lt;/p&gt;
&lt;h3&gt;더 작은 단위의 배포&lt;/h3&gt;
&lt;p&gt;예를 들어서, 한 달이 걸릴 작업 하나를 일주일에 한번 배포하는 방식으로 쪼개서 배포할 수 있습니다. 물론 작업의 유형에 따라 그렇게 하기 상대적으로 더 어려운 경우도 존재하지만, 그렇게 했을 때 얻을 수 있는 이득 또한 분명합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;여러 개의 작은 배포로 큰 배포 하나를 대체함으로서, &lt;em&gt;큰 기능 문제&lt;/em&gt;를 해결할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;의존적인 기능 문제&lt;/em&gt; 또한, 두 기능이 공통적으로 의존하는 부분을 추출해서 먼저 작업해 배포한다면, 두 기능을 실제로 구현하는 것은 그 후에 이어서 독립적으로 진행할 수 있게 됩니다.&lt;/li&gt;
&lt;li&gt;한 번의 배포에 추가되는 변경사항의 개수가 작을수록, 오류가 발생할 확률도 적어지고, 오류가 발생했을 때의 디버깅 또한 용이해집니다. 따라서 &lt;em&gt;불안정한 기능 문제&lt;/em&gt; 의 영향을 최소화할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;피쳐 토글&lt;/h3&gt;
&lt;p&gt;피쳐 토글(Feature Toggle)을 사용한다면, 작성한 코드를 운영 서버에 미리 배포해두고, 이 코드를 실제로 운영 환경에서 실행할지를 원격으로, 별도의 배포 없이 결정할 수 있습니다. 이는 코드의 배치 (deploy)와 기능의 릴리즈 (release)를 분리해서 다룰 수 있게 합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;의존적인 기능 문제&lt;/em&gt; 가 발생했을 때, 먼저 내가 제어할 수 있는 부분만 작업해서 코드를 배치해 두고, 내가 의존해야 하는 부분이 완료된 후에 피처 토글을 통해 기능을 정식으로 릴리즈하는 방식으로 해결할 수 있습니다.&lt;/li&gt;
&lt;li&gt;정확히 구현된 것을 확신하기 어려운 작업 또한, 피쳐 토글로 보호된 상태로 코드만 먼저 배치해 둘 수 있습니다. 그 후 피쳐 토글을 통해 기능을 일시적으로 릴리즈하고, 기능에 문제가 없는 것을 확인한 후 영구적으로 릴리즈하는 방식을 사용할 수 있습니다. 이를 통해 &lt;em&gt;불안정한 기능 문제&lt;/em&gt; 를 해결할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;피쳐 토글에 대해 더 자세히 알고 싶으시다면, 제가 작성한 다른 글인 &lt;a href="https://tech.mfort.co.kr/blog/2022-11-24-feature-toggle/"&gt;피쳐 토글 - 빠르고 안정적인 릴리즈를 향한 도약&lt;/a&gt;도 확인해 보세요!&lt;/p&gt;
&lt;h3&gt;테스트 코드와 통합 단계에서의 테스트 자동화&lt;/h3&gt;
&lt;p&gt;테스트 코드의 중요성은 트렁크 기반 개발에서도 그 빛을 발합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;새로운 기능을 위한 테스트 코드를 작성해 두면, 새로운 기능이 의도와 다르게 동작할 확률을 최소화할 수 있습니다. 또한 그 후에 만약 새로운 기능이 추가되어서 기존 기능에 영향을 주더라도, 테스트 코드를 통해서 이를 감지할 수 있습니다. 이는 자연스럽게 &lt;em&gt;불안정한 기능 문제&lt;/em&gt;를 해결합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;트렁크 기반 개발이 내포하는 근본적인 가치&lt;/h2&gt;
&lt;p&gt;여기까지가 트렁크 기반 개발에 대한 기본적인 설명이었습니다. 여기까지 읽었을 때 어떤 생각이 드셨나요? 트렁크 기반 개발이 좋아 보여서 적용해보고 싶을 수도 있고, 그래도 설득되지 않아서 Git Flow를 그대로 사용하고 싶을 수도 있습니다.&lt;/p&gt;
&lt;p&gt;Git Flow와 트렁크 기반 개발 두 방식 중 어느 하나가 항상 더 좋다고 생각하기 쉽지만, 저는 두 방식 모두 각자의 장점을 취하기 위해 감수하는 단점이 있다고 생각합니다. 아래와 같이 Git Flow와 트렁크 기반 개발이 내포하는 근본적인 가치를 각각 한 문장으로 요약할 수 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Git Flow는 정교하고 안정적이다&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;검증 환경과 운영 환경을 명확히 구분합니다.&lt;/li&gt;
&lt;li&gt;큰 덩어리의 피쳐를 충분히 검증해 한번에 배포합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;트렁크 기반 개발은 간단하고 빠르다&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;검증 환경을 거치지 않고 실제 환경에 바로 배포합니다.&lt;/li&gt;
&lt;li&gt;작은 피쳐 여러개를 각각 최소한으로 검증하고 배포합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;즉, 트렁크 기반 개발은 간단하고 빠른 배포를 추구하기 위해 정교하고 안정적인 배포를 포기한 것이죠.
따라서 트렁크 기반 개발이 추구하는 간단하고 빠른 배포가 잘 들어맞는 환경을 가지고 있을 때, 트렁크 기반 개발을 도입하는 것을 적극적으로 고려해보면 좋을 것이라고 생각합니다. 예를 들어서..&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;애자일 원칙을 따라 지속적으로 작은 변경사항을 고객에게 제공하고 싶을 때&lt;/li&gt;
&lt;li&gt;클라우드에 배포되는 웹 앱과 같이, 배포비용이 작고 롤백이 용이할 때&lt;/li&gt;
&lt;li&gt;페어 프로그래밍, 코드 리뷰, 테스트 코드와 같이 코드의 품질을 끌어올리는 개발 문화에 익숙할 때&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;그래도 두려워요&lt;/h3&gt;
&lt;p&gt;Git Flow에 한계를 느끼고 있고, 트렁크 기반 개발의 가치에 공감할지라도, 아마 트렁크 기반 개발 도입을 주저하게 되는 가장 큰 이유 중에 하나는 “운영배포하는 것이 두려워서” 일 것이라고 생각합니다. 검증도 안 한 기능을 바로 운영에 올리는 것은 당연히 그 위험성이 존재하고, 그게 두려운 건 어떻게 보면 당연한 반응이라고 생각합니다.&lt;/p&gt;
&lt;p&gt;운영 배포가 두렵게 느껴질 때는, 마틴 파울러의 &lt;a href="https://martinfowler.com/bliki/FrequencyReducesDifficulty.html"&gt;FrequencyReducesDifficulty&lt;/a&gt; 글을 읽어보는 것이 도움이 되었습니다. 이 글의 첫 문장에서는 아래와 같이 이야기합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;if it hurts, do it more often.&lt;/p&gt;
&lt;p&gt;어떤 일이 고통스럽다면, 더 자주 해라.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;이 글에서는, 통합은 고통스러운 작업이기 때문에 이를 미룰려고 시도하는 것은 사람이라면 당연한 반응이라고 얘기합니다. 하지만 통합은 미루면 미룰수록 그 난이도가 증가하기 때문에, 오히려 자주 통합해서 자주 고통을 맞이한다면 고통의 총합은 자연스레 줄어들 것이라고 이야기합니다.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tech.mfort.co.kr/assets/images/20220805/freq-reduces-difficulty.png"&gt;
&lt;em&gt;통합이 늦어지면 늦어질수록, 통합의 난이도도 같이 빠르게 증가합니다.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;만약 배포하는 것이 너무나 무섭고 고통스러운 일이라서 트렁크 기반 개발을 도입하기에 어려움을 느낀다면, 트렁크 기반 개발을 통해 더 자주, 더 작게 배포해서 배포의 고통을 줄여보는 것은 어떨까요?&lt;/p&gt;
&lt;h2&gt;맘시터 백엔드팀에서 적용해본 결과&lt;/h2&gt;
&lt;p&gt;여기서부터는 맘시터 백엔드팀에서 트렁크 기반 개발을 적용해보면서 얻은 성과를 자랑해보고자 합니다.&lt;/p&gt;
&lt;p&gt;맘시터 백엔드팀은 트렁크 기반 개발의 실천법을 따라 &lt;code&gt;main&lt;/code&gt; 이라는 주 브랜치 하나를 운용하고 있습니다. 새로운 기능을 만들기 위해서는 &lt;code&gt;main&lt;/code&gt;에서 피쳐 브랜치를 만들어서 작업을 하고, 며칠 내로 PR(Pull Request)을 올려 코드리뷰를 받습니다.&lt;/p&gt;
&lt;p&gt;PR이 올라감과 동시에, Github Action을 통해 PR에 대한 테스트와 코드 품질 분석을 자동으로 실행합니다.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tech.mfort.co.kr/assets/images/20220805/mfort_example_test.png"&gt;
&lt;em&gt;테스트와 코드 품질 분석(SonarCloud)가 모두 성공한 모습&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;테스트가 성공하는 것을 확인 후, PR 작성자는 원하는 시점에 Merge버튼을 눌려 PR을 운영과 스테이지 환경에 동시에 배포할 수 있습니다. 날마다 다르지만, 일반적으로는 하루 평균 5건 내외의 배포가 발생합니다. (백엔드 개발자는 현재 11명입니다.)&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tech.mfort.co.kr/assets/images/20220805/mfort_example_deploy.png"&gt;
&lt;em&gt;Github Action을 통해 최근 배포된 커밋들의 목록&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;트렁크 기반 개발을 적용하며, 백엔드팀에서 몸소 느낄 수 있었던 장점은 아래과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;다른 사람의 작업이 내 작업의 배포 가능 여부에 영향을 끼치는 경우가 거의 없습니다.
    &lt;ul&gt;
&lt;li&gt;Git Flow를 사용할 때는 다른 사람이 작업한 내용이 현재 배포되어도 되는지 등의 커뮤니케이션이 빈번하게 발생했습니다.&lt;/li&gt;
&lt;li&gt;트렁크 기반 개발 시스템에서는 다른 사람의 작업을 신경쓰지 않고, 내 작업을 바로 머지해서 배포하여도 다른 사람의 작업물과 컨플릭트가 발생하는 경우가 아주 적었습니다.&lt;/li&gt;
&lt;li&gt;컨플릭트가 드물게 발생하더라도, 컨플릭트 범위가 아주 작아 주로 몇 분 내로 간단하게 해소할 수 있었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;PR은 일반적으로 300줄 이하의 변경사항을 가져, 팀원들 간에 손쉽게 리뷰할 수 있었습니다.&lt;/li&gt;
&lt;li&gt;작업한 내용이 실제 운영환경에 배포되기까지의 평균 리드타임이 매우 짧아졌습니다.
    &lt;ul&gt;
&lt;li&gt;작업 -&amp;gt; PR 작성 -&amp;gt; 테스트 자동으로 실행 -&amp;gt; 테스트 성공 확인 후 머지 -&amp;gt; 배포 모니터링의 과정을 거치면 작업된 사항을 운영환경에서 바로 확인할 수 있는데, 간단한 수정사항이라면 이 모든 과정을 늦어도 30분 내외로 마무리할 수 있고, 일반적으로는 코드 리뷰를 거치고 PR이 올라간 날 오후나 다음날에 배포되는 것이 일반적이었습니다.&lt;/li&gt;
&lt;li&gt;Git Flow에서도 긴급한 상황에서는 Hotfix 브랜치를 사용해 빠르게 배포할 수 있었겠지만, 차이점이라면 트렁크 기반 개발에서는 어떤 PR이라도 이렇게 빠르게 운영환경에 바로 적용할 수 있다는 점입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;더 이상 운영과 스테이지에 어떤 버전이 배포되었는지 트래킹할 필요가 없습니다. 스테이지와 운영의 코드가 달라서 에러를 재현하기 힘든 일도 없고, 배포된 줄 알았던 브랜치가 알고 보니 수동 배포 버튼을 누르지 않아 배포되지 않았다는 걸 발견하는 일도 없습니다. 스테이지나 운영에서 문제가 발생했을 때, 따로 생각할 필요 없이 &lt;code&gt;main&lt;/code&gt;에서만 디버깅하면 충분합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;마치며&lt;/h2&gt;
&lt;p&gt;이것으로 제 글을 마치겠습니다. 첨언하자면 Git Flow와 트렁크 기반 개발 두 방법 모두 오랜 기간동안 많은 변형이 있었던 전략이고, 위에서 설명한 내용이 모두 항상 들어맞는 것은 아닙니다.&lt;/p&gt;
&lt;p&gt;이 글의 내용은 대략적인 소개로 바라봐주시고, 관심이 있으시다면 다른 글들을 더 읽어보시면서 더 알아보시는 것을 추천드립니다!&lt;/p&gt;
&lt;p&gt;레퍼런스:&lt;/p&gt;
&lt;/div&gt;
</ns0:encoded></item><item><title>Aws 인프라 위에서 채팅 이미지 업로드부터 조회까지</title><link>https://tech.mfort.co.kr/blog/2022-07-12-chat-image-upload/</link><ns0:encoded xmlns:ns0="http://purl.org/rss/1.0/modules/content/">&lt;div class="prose" morss_own_score="2.707165109034268" morss_score="201.40697142707336"&gt;
&lt;p&gt;안녕하세요! 맘편한세상에서 맘시터 서비스를 개발하고 있는 손영철입니다 :)&lt;/p&gt;
&lt;p&gt;맘시터에서는 2022년 6월 부터 부모-시터 회원 간 채팅방에서 이미지를 업로드할 수 있는 기능이 새로 추가되었습니다. 🥳&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tech.mfort.co.kr/assets/images/20220712/chat-room.png"&gt;
&lt;em&gt;채팅방에 새로 적용된 이미지 업로드 기능&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;이번 글에서는 AWS 인프라 위에서 채팅 이미지 업로드부터 조회까지의 기능을 개발하면서 했던 고민과 실제로 운영하면서 겪은 시행착오를 통해 익힌 노하우를 가이드 형식으로 담아보려고 합니다 :)&lt;/p&gt;
&lt;p&gt;그럼 시작합니다!&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;아키텍처&lt;/h2&gt;
&lt;h3&gt;서비스에 맞게 아키텍처를 고려하자&lt;/h3&gt;
&lt;p&gt;아키텍처를 고려할 때 중요한 사항은 &lt;code&gt;서비스의 특성&lt;/code&gt;입니다.&lt;/p&gt;
&lt;p&gt;맘시터 서비스에는 이미지 업로드 기능을 채팅 기능에 추가하였는데요.&lt;/p&gt;
&lt;p&gt;
양방향 통신을 위해 웹소켓을 사용하는 채팅 서버의 경우 &lt;strong&gt;Stateless&lt;/strong&gt;한 API 서버와는 달리 &lt;strong&gt;Stateful&lt;/strong&gt; 하다는 특성이 있기 때문에 &lt;code&gt;안정성&lt;/code&gt;이 중요합니다.  &lt;/p&gt;&lt;p&gt;
그렇기에 채팅 서버가 클라이언트로부터 이미지를 직접 전달받아 스토리지에 업로드 할지 따로 이미지 업로드를 관리하는 서버를 둘지는 서비스 운영에 있어 중요한 기술적 결정사항이었습니다.&lt;/p&gt;&lt;p&gt;트래픽이 몰리는 상황을 가정하였을 때 채팅 서버가 클라이언트로부터 직접 이미지를 받게된다면&lt;br&gt;
&lt;strong&gt;네트워크 병목&lt;/strong&gt; 및 이미지 데이터로 인한 &lt;strong&gt;서버 메모리 사용량 증가&lt;/strong&gt;로 인해 채팅 기능 장애의 원인이 될 수 있다고 판단하여 채팅 서버와 이미지 업로드를 관리하는 서버가 분리된 형태의 아키텍처를 구성하였습니다.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;img src="https://tech.mfort.co.kr/assets/images/20220712/architecture.png"&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;간략하게 도식화된 아키텍처&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;위 아키텍처와 같이 &lt;strong&gt;Stateful&lt;/strong&gt; 한 채팅 서버와 &lt;strong&gt;Stateless&lt;/strong&gt; 한 이미지 서버를 분리하여 이미지 업로드 기능을 많이 사용하더라도 스케일 아웃이 쉽고 트래픽에 유연하게 대응할 수 있도록 구성하였습니다.&lt;/p&gt;
&lt;p&gt;
(추후엔 &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/PresignedUrlUploadObject.html"&gt;S3 presigned URL&lt;/a&gt; 기능을 사용해서 이미지 업로드에 대해서도 서버 부하를 최소한으로 줄이는 방식도 고려하고 있습니다)&lt;/p&gt;&lt;p&gt;이렇듯 아키텍처는 서비스의 특성에 따라 달라질 수 있기 때문에, 서비스의 특성을 잘 파악하고 아키텍처를 설계하시길 바랍니다.&lt;/p&gt;
&lt;h2&gt;S3&lt;/h2&gt;
&lt;h3&gt;Object Key 패턴을 고려하자&lt;/h3&gt;
&lt;p&gt;코드를 작성할 때 모듈과 패키지를 잘 나누는 것이 중요하듯, S3에 업로드되는 Object에 대한 &lt;strong&gt;Key(경로) 패턴&lt;/strong&gt;을 잘 정의하는 것도 중요합니다.&lt;/p&gt;
&lt;p&gt;서비스에 맞게 새로운 종류의 리소스가 추가되거나 기능이 추가되었을 때도 유연하게 대응할 수 있는 Object Key 패턴을 정의하면 리소스 관리에 있어 부채가 줄어들게 됩니다.&lt;/p&gt;
&lt;p&gt;예를 들면 아래와 같이 특정 채팅방과 리소스의 종류로 Object Key 패턴을 정의하면&lt;/p&gt;
&lt;p&gt;
특정 채팅방의 리소스를 리소스 종류 별로 관리할 수 있습니다.&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;/chat/room/&lt;code&gt;{room id}&lt;/code&gt;/&lt;code&gt;{resource type}&lt;/code&gt;/{file name}
    &lt;ul&gt;
&lt;li&gt;/chat/room/9111/image/my_image.jpg&lt;/li&gt;
&lt;li&gt;/chat/room/1109/video/my_video.avi&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Public Access를 최소화하자&lt;/h3&gt;
&lt;p&gt;S3 사용 시 보안을 위해 외부에서 S3 Bucket으로의 직접적인 접근은 막아두는게 좋습니다.&lt;/p&gt;
&lt;p&gt;
가능한 public access를 최소한으로 줄이고 AWS CloudFront 등으로 제한적으로 접근하도록 설정하는 것을 고려하시길 바랍니다.&lt;/p&gt;&lt;p&gt;&lt;em&gt;&lt;img src="https://tech.mfort.co.kr/assets/images/20220712/s3-block.png"&gt;&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;Object metadata 사용 시 영어 외 문자 사용을 유의하자&lt;/h3&gt;
&lt;p&gt;S3 Bucket에 업로드되는 Object에는 &lt;code&gt;x-amz-meta-&lt;/code&gt;를 prefix로 갖는 사용자 정의 metadata를 지정할 수 있는데요.&lt;/p&gt;
&lt;p&gt;
이 사용자 정의 metadata를 통해 업로드한 리소스와 관련된 추가적인 컨텍스트를 Object와 함께 저장할 수 있습니다.&lt;/p&gt;&lt;p&gt;다만 이 Object metadata는 &lt;strong&gt;US-ASCII&lt;/strong&gt; 만 지원하기 때문에,&lt;/p&gt;
&lt;p&gt;
AWS SDK 등을 사용하여 Object metadata를 한글과 같은 non-ASCII 문자로 지정하고 Object를 업로드하는 경우에 오류가 발생할 수 있습니다.&lt;/p&gt;&lt;p&gt;
(관련하여 자세한 내용은 &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingMetadata.html"&gt;AWS 공식 문서&lt;/a&gt;를 확인하시기 바랍니다)&lt;/p&gt;&lt;p&gt;Java 애플리케이션에서 &lt;a href="https://github.com/aws/aws-sdk-java"&gt;aws-sdk-java&lt;/a&gt;를 사용하여 Object의 metadata를 지정하고자 한다면,
&lt;strong&gt;&lt;em&gt;java.net.URLEncoder&lt;/em&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;em&gt;java.net.URLDecoder&lt;/em&gt;&lt;/strong&gt; 를 사용하는 것을 추천합니다.&lt;/p&gt;
&lt;p&gt;(관련해서 &lt;strong&gt;aws-sdk-java&lt;/strong&gt;에 이런 &lt;a href="https://github.com/aws/aws-sdk-java-v2/issues/2624"&gt;이슈&lt;/a&gt;가 올라온 적이 있는데 참고로 살펴보는 것도 좋을 것 같습니다 🙂)&lt;/p&gt;
&lt;h2&gt;CloudFront&lt;/h2&gt;
&lt;h3&gt;CDN 사용을 고려하자&lt;/h3&gt;
&lt;p&gt;정적 리소스는 특성 상 수정보다는 조회가 많은데요.&lt;/p&gt;
&lt;p&gt;
그렇기 때문에 정적 리소스는 빠른 응답을 위해서 주로 &lt;code&gt;CDN&lt;/code&gt;을 캐시 레이어로써 사용합니다.&lt;/p&gt;&lt;p&gt;
빠른 응답 및 외부에서 S3 bucket으로의 직접적인 접근을 막고, 원본 리소스에 대한 부하를 낮추는 용도로도 &lt;strong&gt;CDN&lt;/strong&gt; 사용을 고려하시길 바랍니다.&lt;/p&gt;&lt;p&gt;특히 AWS를 사용한다면 AWS에서 제공하는 &lt;strong&gt;CloudFront&lt;/strong&gt;를 CDN으로 사용하는게 일반적이며 맘시터 서비스 역시 AWS &lt;strong&gt;CloudFront&lt;/strong&gt;를 사용합니다.&lt;/p&gt;
&lt;h3&gt;Behavior 설정을 고려하자&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Behavior&lt;/strong&gt;는 &lt;strong&gt;CloudFront&lt;/strong&gt;가 어떻게 동작하는지에 대한 설정이며, 쉽게 말해 &lt;strong&gt;CDN 캐시에 대한 정책&lt;/strong&gt;이라고 볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;
이러한 Behavior 설정에 대해 충분히 파악한다면 CloudFront 사용에 있어서 다양한 기술적인 선택지를 선택할 수 있습니다.&lt;/p&gt;&lt;p&gt;대표적인 Behavior 설정으로 두가지만 언급하겠습니다.&lt;/p&gt;
&lt;h4&gt;&lt;em&gt;Path pattern&lt;/em&gt;&lt;/h4&gt;
&lt;p&gt;CloudFront는 경로(path)에 따라 캐시 정책을 다르게 가져갈 수 있습니다.&lt;/p&gt;
&lt;p&gt;
리소스 마다 다른 캐시 정책이 필요한 경우 다른 Path pattern을 갖는 Behavior를 여러개 사용하는 방식을 고려할 수 있습니다.&lt;/p&gt;&lt;p&gt;&lt;em&gt;&lt;img src="https://tech.mfort.co.kr/assets/images/20220712/cloudfront-path.png"&gt;&lt;/em&gt;&lt;/p&gt;
&lt;h4&gt;&lt;em&gt;Response headers policy&lt;/em&gt;&lt;/h4&gt;
&lt;p&gt;웹에서 외부 리소스를 요청하기 위해서는 &lt;strong&gt;CORS&lt;/strong&gt; 설정이 필요합니다.&lt;/p&gt;
&lt;p&gt;
웹 서비스거나 앱 내에서 웹 뷰를 사용한다면 &lt;strong&gt;Response headers policy&lt;/strong&gt; 설정을 잊지 마시길 바랍니다.&lt;/p&gt;&lt;p&gt;&lt;em&gt;&lt;img src="https://tech.mfort.co.kr/assets/images/20220712/cloudfront-response-headers-policy.png"&gt;&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;CloudFront에 도메인 네임 지정 시 N.Virginia 리전에 인증서를 등록하자&lt;/h3&gt;
&lt;p&gt;CloudFront는 특정 리전에 종속적인 서비스가 아니기 때문에 &lt;strong&gt;Route53&lt;/strong&gt;에 등록된 domain으로 CloudFront의 &lt;strong&gt;Alternate domain name(CNAME)&lt;/strong&gt;을 설정하려면 &lt;code&gt;N.Virginia(us-east-1)&lt;/code&gt; 리전에 인증서를 등록해야합니다.&lt;/p&gt;
&lt;p&gt;만약 &lt;code&gt;Seoul(ap-northeast-2)&lt;/code&gt; 리전으로 &lt;strong&gt;AWS Certicate Manager&lt;/strong&gt;에 등록된 인증서가 있다면 &lt;code&gt;N.Virgina&lt;/code&gt; 리전으로 스위칭하여 기존 인증서와 동일한 domain name으로 인증서를 등록해주면 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;img src="https://tech.mfort.co.kr/assets/images/20220712/acm.png"&gt;&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;이미지 리사이징&lt;/h2&gt;
&lt;h3&gt;이미지 리사이징을 고려하자&lt;/h3&gt;
&lt;p&gt;이미지 리사이징은 이미지를 보여줘야하지만 항상 원본 이미지를 보여줄 필요가 없는 경우 네트워크 사용량을 줄이기 위해 원본 보다 작은 크기로 이미지를 변환하는 기법입니다.&lt;/p&gt;
&lt;p&gt;채팅방의 이미지와 같이 단순히 보여지는 다수의 이미지를 조회해야하는 경우 이미지 리사이징을 적용한다면 줄어든 데이터의 크기만큼 이미지 로딩이 더 빨리지고, 사용자의 네트워크 사용량을 줄여줄 수 있어 유저 경험을 위해서 고려할 수 있는 방법입니다.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tech.mfort.co.kr/assets/images/20220712/resized-image.png"&gt;&lt;/p&gt;
&lt;h3&gt;이미지 리사이징 방식에 대해 이해하자&lt;/h3&gt;
&lt;p&gt;이미지 리사이징 방식은 여러가지가 있지만 잘 알려진 방식으로는 크게 2가지가 있습니다.&lt;/p&gt;
&lt;h4&gt;&lt;em&gt;이미지 업로드 시 리사이징된 이미지 파일을 생성하는 방식&lt;/em&gt;&lt;/h4&gt;
&lt;p&gt;이미지를 업로드 할 때 원본 이미지와 정해진 크기로 리사이징된 이미지를 추가로 저장하는 방식으로&lt;/p&gt;
&lt;p&gt;
AWS를 사용하는 아키텍처라면 주로 S3 Object 생성 이벤트를 trigger로 하여 Lambda 함수에서 리사이징된 이미지를 생성 후 S3에 업로드하는 방식을 사용합니다.&lt;/p&gt;&lt;p&gt;&lt;img src="https://tech.mfort.co.kr/assets/images/20220712/pre-demand.png"&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;AWS S3와 Lambda를 사용한 이미지 리사이징 아키텍처&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;원본 이미지 업로드와 동시에 리사이징된 이미지가 생성되기 때문에 리사이징된 이미지 조회 시 추가적인 작업 없이 조회할 수 있다는 장점이 있지만,&lt;/p&gt;
&lt;p&gt;
거의 조회되지 않는 이미지에 대해 리사이징을 적용한 경우에는 불필요하게 저장 공간을 낭비할 수 있다는 단점이 있습니다.&lt;/p&gt;&lt;p&gt;
또한 리사이징 해야할 이미지의 크기가 변경되야하는 경우 기존에 리사이징된 이미지에는 변경을 적용하기가 번거롭다는 것이 이 방식의 단점이라고 볼 수 있습니다.&lt;/p&gt;&lt;h3&gt;&lt;em&gt;리사이징된 이미지 요청 시 리사이징된 이미지를 생성하는 방식&lt;/em&gt;&lt;/h3&gt;
&lt;p&gt;온 디맨드(on-demand), 온 더 플라이(on-the-fly) 방식이라고도 불리는 방식으로 리사이징된 이미지 조회 요청이 오면 그때서야 리사이징을 하는 방식입니다.&lt;/p&gt;
&lt;p&gt;앞서 살펴본 리사이징된 이미지를 바로 생성하는 방식에 비해 거의 조회되지 않는 이미지의 리사이징으로 인한 인프라 리소스 낭비를 줄일 수 있다는 장점이 있지만,&lt;/p&gt;
&lt;p&gt;
첫 리사이징된 이미지 요청에 대해서는 리사이징 프로세스가 진행되어야하기 때문에 응답이 늦는 것은 이 방식의 단점입니다.&lt;/p&gt;&lt;p&gt;AWS의 &lt;strong&gt;CloudFront&lt;/strong&gt; + &lt;strong&gt;Lambda@Edge&lt;/strong&gt; 스택을 사용하면 S3에 리사이징된 이미지를 직접 저장하지 않고도 리사이징된 이미지를 &lt;strong&gt;CloudFront&lt;/strong&gt;에 캐싱할 수 있으며&lt;/p&gt;
&lt;p&gt;
만약 캐싱된 이미지의 사이즈를 변경해야하는 케이스가 생긴다면 기존에 &lt;strong&gt;CloudFront&lt;/strong&gt;에 캐싱된 리사이징 이미지 캐시만 삭제하면 되기 때문에 변경에도 유연하게 대응할 수 있습니다.&lt;/p&gt;&lt;p&gt;&lt;img src="https://tech.mfort.co.kr/assets/images/20220712/on-demand.png"&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;AWS CloudFront와 Lambda@Edge를 사용한 이미지 리사이징 아키텍처&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;채팅 서비스의 특성 상 오래된 메세지는 잘 확인하지 않기 때문에, 맘시터 채팅 서비스에서는 이 방식을 채택하여 사용하고 있습니다.
&lt;br&gt;&lt;/p&gt;
&lt;h2&gt;Lambda@Edge&lt;/h2&gt;
&lt;h3&gt;제한사항을 숙지하자&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Lambda@Edge&lt;/strong&gt;는 일반적인 &lt;strong&gt;AWS Lambda&lt;/strong&gt; 와는 다르게 &lt;em&gt;지원하는 언어와 버전&lt;/em&gt;, &lt;em&gt;환경 변수 사용 제약&lt;/em&gt; 등의 제한사항이 있습니다.&lt;/p&gt;
&lt;p&gt;
그렇기 때문에 &lt;strong&gt;Lambda@Edge&lt;/strong&gt;를 사용한 아키텍처를 꾸리기 전에 기술적인 제한사항에 대해 충분히 숙지하시기 바랍니다.&lt;/p&gt;&lt;p&gt;
(특히 이미지 리사이징의 경우 &lt;strong&gt;Origin 응답 변경 시&lt;/strong&gt; 응답의 크기가 &lt;code&gt;1MB&lt;/code&gt;가 넘는지 확인하는 것도 중요합니다)&lt;/p&gt;&lt;p&gt;&lt;em&gt;참고 링크&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;AWS 공식 문서
    &lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/ko_kr/AmazonCloudFront/latest/DeveloperGuide/edge-functions-restrictions.html"&gt;엣지 함수에 대한 제한 사항&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CloudFront Origin Custom Headers&lt;/strong&gt;를 통해 환경 변수 제약을 우회하는 방식
    &lt;ul&gt;
&lt;li&gt;&lt;a href="https://stackoverflow.com/questions/54828808/aws-lambdaedge-nodejs-environment-variables-are-not-supported"&gt;https://stackoverflow.com/questions/54828808/aws-lambdaedge-nodejs-environment-variables-are-not-supported&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;sharp&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://sharp.pixelplumbing.com/"&gt;sharp&lt;/a&gt;는 Node.js 런타임의 Lambda 함수에서 이미지 리사이징 시 자주 사용되는 라이브러리입니다.&lt;/p&gt;
&lt;p&gt;
맘시터 서비스에서도 Lambda@Edge 함수에서 이미지 리사이징을 할 때 &lt;strong&gt;sharp&lt;/strong&gt;를 사용하는데요.&lt;/p&gt;&lt;p&gt;
관련하여 짧게 몇 가지 유의 사항을 공유하겠습니다.&lt;/p&gt;&lt;h3&gt;install 시 platform 설정을 고려하자&lt;/h3&gt;
&lt;p&gt;sharp는 install 시 추가적으로 &lt;strong&gt;platform&lt;/strong&gt; 설정을 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tech.mfort.co.kr/assets/images/20220712/sharp-platform.png"&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;a href="https://sharp.pixelplumbing.com/install#cross-platform"&gt;https://sharp.pixelplumbing.com/install#cross-platform&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;sharp를 사용하는 런타임 환경에 따라 &lt;strong&gt;platform&lt;/strong&gt; 설정을 확인하시길 바랍니다.&lt;/p&gt;
&lt;h3&gt;rotate 설정을 고려하자&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;sharp&lt;/strong&gt;에서 제공하는 rotate 설정은 특정 각도로 이미지를 회전시키는 설정이지만&lt;/p&gt;
&lt;p&gt;
각도를 입력하지 않고 사용하면 이미지의 &lt;code&gt;EXIF&lt;/code&gt; 데이터의 &lt;strong&gt;Orientation tag&lt;/strong&gt;를 확인하여 회전된 이미지와 동일하게 회전시킵니다.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;sharp&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;originalImage&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;.&lt;/span&gt;&lt;span&gt;rotate&lt;/span&gt;&lt;span&gt;()&lt;/span&gt; &lt;span&gt;// &amp;lt;==&lt;/span&gt;
    &lt;span&gt;.&lt;/span&gt;&lt;span&gt;resize&lt;/span&gt;&lt;span&gt;({&lt;/span&gt; &lt;span&gt;width&lt;/span&gt; &lt;span&gt;})&lt;/span&gt;
    &lt;span&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;원본을 회전시킨 이미지를 리사이징 할 때 그 결과물도 동일하게 회전되어있길 원한다면 &lt;strong&gt;rotate&lt;/strong&gt; 설정을 확인하시길 바랍니다.&lt;/p&gt;
&lt;p&gt;(참고: &lt;a href="https://sharp.pixelplumbing.com/api-operation#rotate"&gt;https://sharp.pixelplumbing.com/api-operation#rotate&lt;/a&gt;)&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;마무리&lt;/h2&gt;
&lt;p&gt;정리하고보니 전달하고 싶은 내용이 많아서 글이 길어졌네요.&lt;/p&gt;
&lt;p&gt;이 글을 쓸 수 있었던 것은 단지 저 혼자 모든 것을 리서치하고 개발했기 때문이 아니라 주변 동료들의 조언과 아이디어 덕분입니다.&lt;/p&gt;
&lt;p&gt;
저 혼자서는 이렇게 인프라를 성공적으로 구성하고 운영할 수 없었을 거에요.&lt;/p&gt;&lt;p&gt;
이 자리를 빌어 모든 팀원들에게 감사의 인사를 전달하고 싶습니다 :)&lt;/p&gt;&lt;p&gt;그리고......&lt;/p&gt;
&lt;p&gt;
빠르게 성장 중인 맘시터 서비스를 함께 만들어갈 동료를 찾고있습니다!!&lt;/p&gt;&lt;p&gt;
공고는 &lt;a href="https://www.mfort.co.kr/#8036faa5457d4b36848eaca7e8996d3a"&gt;여기&lt;/a&gt;에서 확인하실 수 있습니다! (많은 관심 부탁드릴게요!!)&lt;/p&gt;&lt;p&gt;마지막으로 긴 글 읽어주셔서 감사합니다 :)&lt;/p&gt;
&lt;/div&gt;
</ns0:encoded></item><item><title>검색 기능 트러블슈팅기 - QueryDSL Cast</title><link>https://tech.mfort.co.kr/blog/2022-06-15-search-function-troubleshooting-query-dsl-cast/</link><ns0:encoded xmlns:ns0="http://purl.org/rss/1.0/modules/content/">&lt;div class="prose" morss_own_score="2.6799999999999997" morss_score="85.68"&gt;
&lt;img src="https://user-images.githubusercontent.com/37354145/171799560-bee61efb-70d7-4613-8174-4ed86a6169c0.png"&gt;
&lt;blockquote&gt;
&lt;p&gt;이전 글: &lt;a href="https://tech.mfort.co.kr/blog/2022-06-14-search-function-troubleshooting-data-class-equals"&gt;검색 기능 트러블슈팅기 - 동등성 비교&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;🔍 트러블 발생&lt;/h2&gt;
&lt;p&gt;동등성 비교 문제를 해결하고, 성공적으로 배포까지 완료했습니다!
매칭 매니저분들께서 활용하실 어드민 페이지에 API를 연결하고 동작 결과를 확인하려 하는데…
어드민 페이지 속 스피너가 한~참을 도네요…?&lt;/p&gt;
&lt;p&gt;&lt;img src="https://user-images.githubusercontent.com/37354145/173180594-ad0a506b-1b71-4f49-922d-4ae42e761e1d.png"&gt;&lt;/p&gt;
&lt;p&gt;약 6~7초가 지나서야 검색 결과를 조회할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.thinkwithgoogle.com/intl/en-ca/marketing-strategies/app-and-mobile/mobile-page-speed-new-industry-benchmarks/"&gt;페이지 로딩이 3초 이상 걸리면 이용자의 50%가 이탈&lt;/a&gt;
하는 시대에 7초가 걸리는 검색 기능을 제공한다는 건 말도 안 되는 일,
곧바로 쿼리의 병목이 발생하는 지점을 찾아 나섰습니다.&lt;/p&gt;
&lt;h2&gt;🔍 병목 지점 찾기&lt;/h2&gt;
&lt;p&gt;&lt;img src="https://user-images.githubusercontent.com/37354145/173178574-a76fc1e2-f3a2-4060-b528-ec7e01bc6251.png"&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;MomsitterUser&lt;/strong&gt;와 &lt;strong&gt;SitterCareManager&lt;/strong&gt;에 대한 인덱스 테이블이 빠져있긴 했으나,
인덱스 테이블이 없다고 해서 30만건도 안되는 데이터를 조회하는 데 7초가 걸릴 리는 없을 거 같았습니다.
원인이 가늠이 가지 않던 중에, 현웅님께 질문을 드린 후 쉽게 해결책을 찾았던 기억이 떠올라
옆자리에서 한참을 집중 중이신 경수님께 무작정 질문을 드렸습니다.&lt;/p&gt;
&lt;p&gt;“경수님, 돌봄 신청 어드민 페이지에서 목록 조회하는 쿼리가 6~7초가 걸리는데 어떤 게 문제일까요…?”&lt;/p&gt;
&lt;p&gt;
“7초?? ㅋㅋㅋ 쿼리 한번 넘겨주세요.”&lt;/p&gt;&lt;p&gt;경수님께서는 곧바로 &lt;strong&gt;explain&lt;/strong&gt; 명령을 통해 쿼리의 실행계획을 살펴보셨습니다.
쿼리 내부에서 인덱스 테이블이 있음에도 인덱스를 타지 못하는 모습을 발견할 수 있었는데,
그걸 통해 7초가 소요되는 이유를 추측할 수 있었어요.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://user-images.githubusercontent.com/37354145/173178640-b506551c-ad83-4545-98d8-baa6218fd46f.png"&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;MomsitterUser&lt;/strong&gt; 엔티티는 이전에 탈퇴한 회원의 정보를 클렌징하는 과정에서
PK의 데이터 타입을 &lt;strong&gt;Long&lt;/strong&gt;에서 &lt;strong&gt;Integer&lt;/strong&gt;로 바꾼 이력을 갖고 있습니다.
이 때문에 QueryDSL을 작성할 때 &lt;strong&gt;MomsitterUser&lt;/strong&gt;의 PK값을 &lt;strong&gt;id.longValue()&lt;/strong&gt;을 통해 변환했는데,
변환이 오래걸리기 때문에 병목이 발생한 것으로 추측했습니다.
(인덱스를 태우는 것 자체도 성능개선이 가능하지만 드라마틱한 변화는 없을 것으로 예상)&lt;/p&gt;
&lt;p&gt;&lt;img src="https://user-images.githubusercontent.com/37354145/173178656-2431bed2-629c-45af-9945-30956e4adff7.png"&gt;&lt;/p&gt;
&lt;p&gt;실제로 하이버네이트가 날리는 쿼리에서 cast 동작을 확인할 수 있었습니다!
30만개의 데이터에서 모두 cast가 발생하니, 7초짜리 쿼리가 탄생할 수밖에 없습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://user-images.githubusercontent.com/37354145/173178657-d693772b-7d66-4583-8566-54e1e3436e89.png"&gt;&lt;/p&gt;
&lt;p&gt;결국 &lt;strong&gt;id.longValue()&lt;/strong&gt; 없이도 &lt;strong&gt;MomsitterUser&lt;/strong&gt;와 &lt;strong&gt;ParentCareApply&lt;/strong&gt;를 조인시킬 방법을 구상해야했습니다.&lt;/p&gt;
&lt;h2&gt;🔍 해결 방법&lt;/h2&gt;
&lt;p&gt;처음엔 &lt;strong&gt;MomsitterUser&lt;/strong&gt; 엔티티의 PK를 &lt;strong&gt;Integer&lt;/strong&gt;에서 &lt;strong&gt;Long&lt;/strong&gt;으로 되돌리는 방법을 떠올렸습니다.
어차피 대부분의 엔티티가 PK를 &lt;strong&gt;Long&lt;/strong&gt;으로 가지고 있고, &lt;strong&gt;MomsitterUser&lt;/strong&gt; 엔티티 역시
언젠가는 &lt;strong&gt;Integer&lt;/strong&gt;에서 &lt;strong&gt;Long&lt;/strong&gt;으로 마이그레이션이 진행되어야하는데
‘이왕 하는 김에 그걸 내가 하면 되겠지.’ 라는 생각이었죠.&lt;/p&gt;
&lt;p&gt;때문에 &lt;strong&gt;MomsitterUser&lt;/strong&gt; 클렌징을 진행하셨던 민영님께 DM을 드렸는데, 민영님께서 겁을 주시네요…😱&lt;/p&gt;
&lt;p&gt;&lt;img src="https://user-images.githubusercontent.com/37354145/173178662-03c3e342-3fb4-4f37-8811-46e475c62bfd.png"&gt;&lt;/p&gt;
&lt;p&gt;실제로 &lt;strong&gt;MomsitterUser&lt;/strong&gt; 엔티티를 살펴보다가 연관관계를 맺고 있는 &lt;strong&gt;MomsitterUserRole&lt;/strong&gt; 엔티티를 발견했고,
이 녀석 또한 PK를 &lt;strong&gt;Integer&lt;/strong&gt;로 갖고 있음을 확인했습니다.&lt;/p&gt;
&lt;p&gt;‘서버 전체에서 전역적으로 사용되는 엔티티의 PK를 바꾸는 일이라 큰 공사일 것임은 예상했지만, 
이 정도 일 줄은… 그래도 할 수 있겠지?’&lt;/p&gt;
&lt;p&gt;막연한 생각을 갖고 있었는데, &lt;strong&gt;MomsitterUserRole&lt;/strong&gt; 외에 다른 연관관계 엔티티도 
&lt;strong&gt;Integer&lt;/strong&gt; PK로 연결되어 있다는 이야기를 접하니 도저히 엄두가 안 납니다…&lt;/p&gt;
&lt;p&gt;&lt;img src="https://user-images.githubusercontent.com/37354145/173178666-59832209-42b6-423d-a18d-71b830b0a0eb.png"&gt;&lt;/p&gt;
&lt;p&gt;고심하다가 &lt;strong&gt;ParentCareApply&lt;/strong&gt; 엔티티에서 &lt;strong&gt;MomsitterUser&lt;/strong&gt;의 PK를
&lt;strong&gt;Integer&lt;/strong&gt;로 변경하고, 이를 꺼내서 사용하는 부분만(getter/setter) &lt;strong&gt;Long&lt;/strong&gt; 타입을 사용하도록 하여 해결했습니다.
이를 통해 QueryDSL 조인 파트에서는 &lt;strong&gt;Integer&lt;/strong&gt; 타입 PK 끼리 성공적으로 조인이 이루어지고,
코틀린 코드에서는 &lt;strong&gt;Long&lt;/strong&gt; 타입을 통한 핸들링을 이전처럼 진행할 수 있게 됩니다.
(추후 PK의 값이 커지면 역시나 &lt;strong&gt;Long&lt;/strong&gt;으로 마이그레이션을 진행해야 하긴 하지만…)&lt;/p&gt;
&lt;p&gt;&lt;img src="https://user-images.githubusercontent.com/37354145/173178718-a74f4747-234a-4155-8b58-6c7f650b375c.png"&gt;&lt;/p&gt;
&lt;p&gt;결국 cast 동작이 제거되어 10배의 성능 개선 결과를 얻을 수 있었고,
쾌적한 돌봄 신청 목록 조회가 가능해졌어요!&lt;/p&gt;
&lt;h2&gt;🔍 무얼 배웠는가&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;개발 히스토리를 알고 있는 사람은 중요하다.&lt;/li&gt;
&lt;li&gt;몇십만 건에 해당하는 데이터 조회는 생각보다 오래 걸리지 않는다.&lt;/li&gt;
&lt;li&gt;몇십만 건에 해당하는 데이터에 인덱스가 드라마틱한 변화를 주진 않는다.&lt;/li&gt;
&lt;li&gt;항상 쿼리 실행계획을 잘 살펴보자. 인덱스 테이블을 만들 때만 유용한 게 아니다.&lt;/li&gt;
&lt;li&gt;곧장 경수, 민영님께 질문드린 건 잘한 거 같다! 항상 질문하자!&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;해피엔딩!&lt;/p&gt;
&lt;p&gt;&lt;img src="https://user-images.githubusercontent.com/37354145/173178724-331287fc-7209-4443-a276-ea0c153af864.png"&gt;&lt;/p&gt;
&lt;p&gt;항상 감사합니다 경수님, 민영님 🙇‍♂️&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.thinkwithgoogle.com/intl/en-ca/marketing-strategies/app-and-mobile/mobile-page-speed-new-industry-benchmarks/"&gt;Find Out How You Stack Up to New Industry Benchmarks for Mobile Page Speed&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
</ns0:encoded></item><item><title>검색 기능 트러블슈팅기 - 동등성 비교</title><link>https://tech.mfort.co.kr/blog/2022-06-14-search-function-troubleshooting-data-class-equals/</link><ns0:encoded xmlns:ns0="http://purl.org/rss/1.0/modules/content/">&lt;div class="prose" morss_own_score="2.662809917355372" morss_score="113.16280991735537"&gt;
&lt;img src="https://user-images.githubusercontent.com/37354145/171799560-bee61efb-70d7-4613-8174-4ed86a6169c0.png"&gt;
&lt;p&gt;맘시터 매칭 매니저분들은 부모님이 내 아이와 잘 어울리는 시터를 더 빨리, 더 편하게 만날 수 있도록 밤낮으로 노력하고 계십니다.
코어플랫폼개발팀에서는 이런 매칭 매니저분들의 수고를 덜어드리고자 기존 프로세스들을 개선 중인데요,
그 과정에서 겪은 트러블슈팅 내용을 공유해 드리고자 합니다.&lt;/p&gt;
&lt;h2&gt;🔍 트러블 발생&lt;/h2&gt;
&lt;p&gt;매칭 매니저분들이 새롭게 사용하실 어드민 페이지 오픈이 코앞까지 다가왔습니다.
어드민 페이지에서 &lt;strong&gt;날짜, 신청 상태, 담당 매니저&lt;/strong&gt;라는 3가지 조건으로
원하는 결과를 빨리 찾으실 수 있도록 검색 기능을 구현 중인데요,
검색 기능 구현을 위해서 QueryDSL 사용을 고려했습니다.
검색 조건으로 사용되는 데이터들이 엔티티 4개를 조합(&lt;strong&gt;JOIN&lt;/strong&gt;)한 분량이라는 점과
그것들을 모두 반환하기엔 불필요하게 노출되는 정보가 너무 많다는 점 때문입니다.
QueryDSL을 사용해 조회된 결과를 별도의 data class(&lt;strong&gt;CareApplyAndScheduleAndSitterDTO&lt;/strong&gt;)에 담아 반환하도록
기능을 구현했습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://user-images.githubusercontent.com/37354145/173176999-2e9316ad-d712-45b4-bcc3-88c29b94bbac.png"&gt;&lt;/p&gt;
&lt;p&gt;조회에 필요한 정보만 프로퍼티로 가진 data class를 준비하고
그 data class에 정보를 담아 전달 할 수 있는 쿼리를 작성한 뒤,
조회가 성공할 것이라는 확신과 함께 기쁜 마음으로 테스트를 실행했는데
예상치 못한 부분에서 테스트가 실패했습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://user-images.githubusercontent.com/37354145/171800690-dd12ae0f-e336-44b0-8d68-015835c44959.png"&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://jojoldu.tistory.com/394"&gt;QueryDSL은 WHERE절에 null을 전달하는 경우 조건문을 아예 생성하지 않습니다.&lt;/a&gt;
때문에 검색 조건을 모두 &lt;strong&gt;null&lt;/strong&gt;로 전달했을 경우 성공할 수밖에 없는 테스트입니다.
마침 조회되어야 할 원소의 개수는 7개고, &lt;strong&gt;조회결과_리스트&lt;/strong&gt;의 크기도 7인데?!
테스트는 실패했습니다.&lt;/p&gt;
&lt;p&gt;data class 내부 모든 원소 역시 primitive type 이거나 data class 였기 때문에 동등성 비교로 문제가 생길 리는 없습니다.
분명 논리적으로 문제가 되는 부분이 없는데, 컴파일러는 계속해서 데이터가 일치하지 않는다고 우기고 있었습니다.&lt;/p&gt;
&lt;p&gt;홧김에 ‘테스트고 뭐고 데이터 잘 나오는 건 확실한데 그냥 배포할까…’ 생각도 들었지만,
혹여나 검색 로직이 변경된다면 든든한 방패가 되어줄 테스트임이 분명했습니다.
결국 모든 원소를 하나하나 비교해가며 테스트를 통과시키기 위한 여정을 시작했습니다.&lt;/p&gt;
&lt;h2&gt;🔍 왜 동등성 비교에 실패하지?&lt;/h2&gt;
&lt;p&gt;&lt;img src="https://user-images.githubusercontent.com/37354145/171801794-4aaccd15-8eeb-4208-acc4-c821e129cb59.png"&gt;&lt;/p&gt;
&lt;p&gt;우선 ‘어떤 녀석이 일치하지 않는가?’부터 시작했습니다.
&lt;strong&gt;shouldContainExactlyInAnyOrder&lt;/strong&gt;의 대상이 되는 원소들을 하나씩 분리 후
&lt;strong&gt;shouldContain&lt;/strong&gt;를 통해 리스트에 포함되어 있는지 테스트를 진행해보았습니다.
그러자 공통으로 &lt;strong&gt;돌봄희망일&lt;/strong&gt;을 가진 경우에 동등성 비교에 실패하는 걸 확인할 수 있었는데요,
&lt;strong&gt;돌봄희망일 == 스케줄&lt;/strong&gt;이었으므로, 스케줄 정보를 가지고 있는 data class에서 동등성 비교가 실패함을 추측 가능했습니다.&lt;/p&gt;
&lt;p&gt;이번에는 &lt;strong&gt;shouldBe&lt;/strong&gt;로 대놓고 동등성 비교를 테스트해보았습니다.
그러나 동등성 비교를 테스트하면서 더욱 미궁속으로 빠져들었는데,
동등성 비교 실패로 보여준 정보가 서로 완전히 완전히 동일했기 때문입니다.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://user-images.githubusercontent.com/37354145/171811521-e8176d78-bd82-4370-8df9-df375aad07f2.png"&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;아주 미치고 펄쩍 뛸 노릇!!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;일반 class가 아닌 data class라서 동등성 비교를 진행할 게 분명하고,
보기엔 완전히 동일한 데이터인데 동등성 비교에 실패한다니.
‘혹시 data class를 잘못 사용한 게 아닐까?’ 걱정되어
완전히 동일한 data class 인스턴스를 2개 만들어 비교도 해보았습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://user-images.githubusercontent.com/37354145/171813595-71d0a92b-1bd7-4d62-8337-a9f08e4345c2.png"&gt;&lt;/p&gt;
&lt;p&gt;그러자 이번에는 통과하는 모습을 보여줬습니다.
이즈음부터 ‘영속성 컨텍스트를 다녀온 data class에는 무언가 변화가 생기는가?’ 의심이 피어났지만,
‘영속성 컨텍스트를 다녀와 봐야 data class는 data class지.’ 라며 의심을 접고
다시 data class 내부 원소들을 하나씩 뜯어보기 시작했습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://user-images.githubusercontent.com/37354145/171803218-26c397b1-6d41-495d-b59a-be8f5c17118c.png"&gt;
&lt;img src="https://user-images.githubusercontent.com/37354145/171811456-5f35690d-bbd1-4bc2-85b7-f62615d77261.png"&gt;&lt;/p&gt;
&lt;p&gt;우선 스케줄 data class인 &lt;strong&gt;RegularScheduleDto&lt;/strong&gt;와 그 내부 프로퍼티들을 하나하나 테스트해 보았습니다.
그 결과 &lt;strong&gt;RegularSchedule&lt;/strong&gt; 세부 정보에 해당하는 &lt;strong&gt;RegularScheduleOption&lt;/strong&gt; data class 에서 문제가 발생하고 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://user-images.githubusercontent.com/37354145/171812061-d309f088-e0ee-4c16-896c-ef8c4947ab92.png"&gt;&lt;/p&gt;
&lt;p&gt;그러나 &lt;strong&gt;RegularScheduleOption&lt;/strong&gt; data class만 별도로 동등성 비교를 진행해봐도 여전히 원인을 파악할 수 없었습니다.
‘혹시나 &lt;strong&gt;DayOfWeek&lt;/strong&gt; 클래스 간 동등성 비교가 안 되는 건 아닐까?’, ‘&lt;strong&gt;LocalTime&lt;/strong&gt; 소수점 값에 차이가 있던 건 아닐까?’ 등
여러 가지 테스트를 추가로 진행해보았지만, 진전이 없었습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://user-images.githubusercontent.com/37354145/173177544-34d9ef65-7ff8-4967-b31d-a47e5f78bd4d.png"&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;RegularScheduleDto 짜증난다…&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;🔍 팀원분들께 도움을 받자&lt;/h2&gt;
&lt;p&gt;결국 문제를 해결하지 못하고 하루를 통째로 날렸습니다.
다음 날 점심시간 내려가는 엘리베이터에서 팀원분들께 현 상황을 툴툴거리며 말씀드렸더니
곰곰이 듣고 계시던 20년 차 백엔드 개발자 현웅님께서 여러 가지 접근 방법을 제시해주셨습니다.
점심식사 후 현웅님이라면 해결의 실마리를 잡아주실 수도 있을 거라는 기대감에 무작정 노트북을 들고 찾아갔습니다.
한참 채팅 기능 개선으로 바쁘신 와중에도 흔쾌히 문제를 같이 살펴봐 주셨고,
10분도 안 되어 실마리를 잡아내 주셨습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://user-images.githubusercontent.com/37354145/171814389-0286a5f1-c9d4-40b5-9926-dc6c44e7912b.png"&gt;&lt;/p&gt;
&lt;p&gt;현웅님은 디버깅 모드로 데이터 타입부터 확인해주셨습니다.
‘눈에 보이는 데이터가 모두 일치하는 데 동등성 비교가 실패한다면,
데이터의 타입이 달라서 동등성 비교를 시도조차 하지 않는다.’는 접근이셨던거 같아요.
실제로 &lt;strong&gt;RegularScheduleDto&lt;/strong&gt; 내부 &lt;strong&gt;timeSlots&lt;/strong&gt; 컬렉션의 데이터 타입이 달랐습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://user-images.githubusercontent.com/37354145/173177884-2bcc385e-bd7b-434b-ad84-ba042c631a19.png"&gt;&lt;/p&gt;
&lt;p&gt;영속성 컨텍스트에 진입하지 않은 컬렉션은 본래 데이터 타입을 유지하고 있지만,&lt;/p&gt;
&lt;p&gt;&lt;img src="https://user-images.githubusercontent.com/37354145/173177892-87fdc0f8-cad9-4a54-99de-3275fdc1c855.png"&gt;&lt;/p&gt;
&lt;p&gt;영속성 컨텍스트에 진입한 컬렉션은 &lt;strong&gt;PersistentBag&lt;/strong&gt;으로 감싸집니다.
&lt;a href="https://joont92.github.io/jpa/%EC%BB%AC%EB%A0%89%EC%85%98%EA%B3%BC-%EB%B6%80%EA%B0%80%EA%B8%B0%EB%8A%A5/"&gt;JPA가 영속성 컨텍스트에 속한 컬렉션을 보다 쉽게 관리하기 위해서 감싸는 것!&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://user-images.githubusercontent.com/37354145/173177905-ac1f3f74-dac7-47c5-a124-d5a0f7f2cbdd.png"&gt;&lt;/p&gt;
&lt;p&gt;결국 &lt;strong&gt;PersistentBag&lt;/strong&gt;과 &lt;strong&gt;ArrayList&lt;/strong&gt;로 데이터 타입부터가 다르니까 동등성 비교에 실패하는 게 당연합니다.&lt;/p&gt;
&lt;h2&gt;🔍 해결 방법&lt;/h2&gt;
&lt;p&gt;원인이 명확히 밝혀지니 해결은 아~주 수월합니다.
&lt;strong&gt;엔티티 -&amp;gt; data class&lt;/strong&gt;로 변환할 때 &lt;strong&gt;toList()&lt;/strong&gt; 메서드를 통해 컬렉션으로 변환을 명시적으로 하거나,
테스트 코드에서 data class간 동등성 비교가 아닌 data class 내부 값을 직접 꺼내서 비교를 진행하면
문제를 해결할 수 있습니다.&lt;/p&gt;
&lt;p&gt;저의 경우 실제로 API 호출 시 &lt;strong&gt;CareApplyAndScheduleAndSitterDTO&lt;/strong&gt;를 반환하므로
&lt;strong&gt;CareApplyAndScheduleAndSitterDTO&lt;/strong&gt;를 비교하는 테스트가 더 의미 있다고 생각해서
&lt;strong&gt;toList()&lt;/strong&gt; 메서드를 통해 명시적으로 컬렉션 반환을 하도록 했습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://user-images.githubusercontent.com/37354145/171815352-cf747d54-ae97-418f-9c02-b77e902eb2a3.png"&gt;
&lt;img src="https://user-images.githubusercontent.com/37354145/173177918-42ade696-bd31-49b2-b0d6-754cfdaaaad6.png"&gt;&lt;/p&gt;
&lt;p&gt;감사합니다 현웅님! 🙇‍♂️&lt;/p&gt;
&lt;h2&gt;🔍 무얼 배웠는가&lt;/h2&gt;
&lt;p&gt;‘왜 동등성 비교가 안 될까?’, ‘&lt;strong&gt;equals&lt;/strong&gt;나 &lt;strong&gt;shouldBe&lt;/strong&gt;가 다르게 동작하는 부분이 있나?’ 와 같이
익숙한 부분에서만 원인을 찾으려 했던 점이 아쉽습니다.
수  많은 개발자가 믿고 사용하는 &lt;strong&gt;equals&lt;/strong&gt;와 &lt;strong&gt;shouldBe&lt;/strong&gt; 등을 의심했고,
의심 과정에서 그간 알고 있던 개념이 흐트러질까 봐 혼란스러워하고 스트레스를 받았어요.
그러나 현웅님께서는 자연스럽게 hashCode 부터 확인하셨습니다.
모두가 믿고 사용하는 라이브러리에는 원인이 없을게 당연하니
디버깅 모드로 문제가 발생할 수 있는 영역만 체크하신 거 같아요.
결국 정리하면 아래와 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;모두가 믿고 사용하는 것에는 이유가 있다.&lt;/li&gt;
&lt;li&gt;디버깅 모드와 더욱 친해져 보자.&lt;/li&gt;
&lt;li&gt;모르는 게 있다면 주변 동료분들께 더더욱 적극적으로 도움을 요청하자.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://tech.mfort.co.kr/blog/2022-06-15-search-function-troubleshooting-query-dsl-cast"&gt;이렇게 해피엔딩이 되나 싶었지만, 배포 후 또 다른 문제를 마주치게 되었습니다…&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;/div&gt;
</ns0:encoded></item></channel></rss>